package org.javarosa.core.services.mesh4j.extensions;

import java.util.Date;
import java.util.Vector;

import javax.microedition.rms.RecordStore;

import org.javarosa.core.JavaRosaServiceProvider;
import org.javarosa.core.model.storage.DataModelTreeRMSUtility;
import org.javarosa.core.model.storage.FormDefRMSUtility;
import org.javarosa.core.services.IService;
import org.javarosa.core.services.UnavailableServiceException;
import org.javarosa.core.services.properties.mesh4j.extensions.Mesh4jPropertyRules;
import org.javarosa.mesh4j.sync.adapter.DataModelTreeContentAdapter;
import org.javarosa.mesh4j.sync.adapter.FormDefContentAdapter;
import org.javarosa.mesh4j.sync.rms.ConsoleRMSStorage;
import org.javarosa.mesh4j.sync.security.LoggedUserIdentityProvider;
import org.javarosa.xform.mesh4j.extensions.FormDefUtils;
import org.javarosa.xform.util.XFormUtils;
import org.mesh4j.sync.SyncEngine;
import org.mesh4j.sync.adapters.feed.atom.AtomSyndicationFormat;
import org.mesh4j.sync.adapters.http.HttpSyncAdapter;
import org.mesh4j.sync.adapters.rms.storage.RmsStorageSyncRepository;
import org.mesh4j.sync.adapters.split.SplitAdapter;
import org.mesh4j.sync.id.generator.IdGenerator;
import org.mesh4j.sync.message.IEndpoint;
import org.mesh4j.sync.message.IMessage;
import org.mesh4j.sync.message.IMessageSyncAdapter;
import org.mesh4j.sync.message.IMessageSyncAware;
import org.mesh4j.sync.message.ISyncSession;
import org.mesh4j.sync.message.MessageSyncEngine;
import org.mesh4j.sync.message.channel.sms.ISmsChannel;
import org.mesh4j.sync.message.channel.sms.ISmsRetiesNotification;
import org.mesh4j.sync.message.channel.sms.SmsChannelFactory;
import org.mesh4j.sync.message.channel.sms.SmsEndpoint;
import org.mesh4j.sync.message.channel.sms.batch.SmsMessageBatch;
import org.mesh4j.sync.message.channel.sms.connection.ISmsConnectionInboundOutboundNotification;
import org.mesh4j.sync.message.channel.sms.connection.SmsConnection;
import org.mesh4j.sync.message.channel.sms.factory.SmsMessageSyncEngineFactory;
import org.mesh4j.sync.message.core.MessageSyncAdapter;
import org.mesh4j.sync.message.core.repository.IMessageSyncAdapterFactory;
import org.mesh4j.sync.message.encoding.CompressBase64MessageEncoding;
import org.mesh4j.sync.message.protocol.ACKEndSyncMessageProcessor;
import org.mesh4j.sync.message.protocol.ACKMergeMessageProcessor;
import org.mesh4j.sync.message.protocol.BeginSyncMessageProcessor;
import org.mesh4j.sync.message.protocol.CancelSyncMessageProcessor;
import org.mesh4j.sync.message.protocol.EndSyncMessageProcessor;
import org.mesh4j.sync.message.protocol.GetForMergeMessageProcessor;
import org.mesh4j.sync.message.protocol.ItemEncoding;
import org.mesh4j.sync.message.protocol.LastVersionStatusMessageProcessor;
import org.mesh4j.sync.message.protocol.MergeMessageProcessor;
import org.mesh4j.sync.message.protocol.MergeWithACKMessageProcessor;
import org.mesh4j.sync.message.protocol.NoChangesMessageProcessor;
import org.mesh4j.sync.model.Item;
import org.mesh4j.sync.validations.MeshException;

public class SyncEngineService implements IService, IMessageSyncAdapterFactory, IMessageSyncAware, ISmsConnectionInboundOutboundNotification, ISmsRetiesNotification {
 
	private static final String SYNC_INFO_PREFIX = "M4X_SYNC_INFO_";
	private static final String FORM_DEF_XFORM_NAME = "FormDef";
	private final static String NAME = "MessageSyncEngineService";
	private final static Object SEMAPHORE = new Object();
		
	// MODEL VARIABLES
	private MessageSyncEngine smsSyncEngine;
	private SmsConnection smsConnection;
	private ConsoleRMSStorage consoleRepository;
	
	// BUSINESS METHODS
	
	public SyncEngineService(){
		super();
		initialize();
	}

	private void initialize() {
		this.consoleRepository = new ConsoleRMSStorage();
		
		String smsPort = Mesh4jPropertyRules.getSmsPor();
		int receiverRetryTimeOut = Mesh4jPropertyRules.getReceiverRetryTimeOut();
		int senderRetryTimeOut = Mesh4jPropertyRules.getSenderRetryTimeOut();
		int blockDiff = Mesh4jPropertyRules.getBlockDiff();
		int maxMessageLenght = Mesh4jPropertyRules.getMaxMessageLenght();
		
		this.smsConnection = new SmsConnection("sms://:"+smsPort, maxMessageLenght, CompressBase64MessageEncoding.INSTANCE, this);
		this.smsSyncEngine = SmsMessageSyncEngineFactory.makeSyncEngine(this.smsConnection, this, this, senderRetryTimeOut, receiverRetryTimeOut, blockDiff, this, LoggedUserIdentityProvider.INSTANCE, IdGenerator.INSTANCE);
		this.smsConnection.startConnection();
	}
	
	public void restart(){
		int receiverRetryTimeOut = Mesh4jPropertyRules.getReceiverRetryTimeOut();
		int senderRetryTimeOut = Mesh4jPropertyRules.getSenderRetryTimeOut();
		SmsChannelFactory.restart(senderRetryTimeOut, receiverRetryTimeOut);
	}

	// IService METHODS
	public String getName() {
		return NAME;
	}
	
	public void shutDown() {
		this.smsConnection.closeConnection();
	}
	
	// SYnc METHODS
	public static Vector<Item> synchronizeItemsSynchronousHttpMethod(String formName) {
		String url = getBaseURL()+ formName + "?format=atom10";
		SplitAdapter splitAdapter = makeXFormSplitAdapter(formName);
		HttpSyncAdapter httpAdapter = new HttpSyncAdapter(url, AtomSyndicationFormat.INSTANCE, LoggedUserIdentityProvider.INSTANCE, IdGenerator.INSTANCE);
		SyncEngine syncEngine = new SyncEngine(splitAdapter, httpAdapter);
		Vector<Item> conflicts = syncEngine.synchronize();
		return conflicts;
	}

	public static SplitAdapter makeXFormSplitAdapter(String formName) {  // TODO (JMT) storage name without formName
		RmsStorageSyncRepository syncRepo = new RmsStorageSyncRepository(SYNC_INFO_PREFIX+formName, LoggedUserIdentityProvider.INSTANCE, IdGenerator.INSTANCE);
		DataModelTreeContentAdapter contentAdapter = new DataModelTreeContentAdapter(formName);
		SplitAdapter splitAdapter = new SplitAdapter(syncRepo, contentAdapter, LoggedUserIdentityProvider.INSTANCE);
		return splitAdapter;
	}
	
	public static SplitAdapter makeXFormDefSplitAdapter() {
		RmsStorageSyncRepository syncRepo = new RmsStorageSyncRepository(SYNC_INFO_PREFIX+FORM_DEF_XFORM_NAME, LoggedUserIdentityProvider.INSTANCE, IdGenerator.INSTANCE);
		FormDefContentAdapter contentAdapter = new FormDefContentAdapter();
		SplitAdapter splitAdapter = new SplitAdapter(syncRepo, contentAdapter, LoggedUserIdentityProvider.INSTANCE);
		return splitAdapter;
	}

	private static String getBaseURL() {
		return JavaRosaServiceProvider.instance().getPropertyManager().getSingularProperty(Mesh4jPropertyRules.HTTP_SYNC_BASE_URL);
	}

	public static void synchronizeItemsAsynchronousSmsMethod(String formName, String smsNumber) throws UnavailableServiceException {
		SyncEngineService syncEngine = (SyncEngineService) JavaRosaServiceProvider.instance().getService(SyncEngineService.NAME);
		if(syncEngine != null){
			try{
				SplitAdapter splitAdapter = makeXFormSplitAdapter(formName);
				MessageSyncAdapter adapter = new MessageSyncAdapter(formName, LoggedUserIdentityProvider.INSTANCE, splitAdapter);
				syncEngine.smsSyncEngine.synchronize(adapter, new SmsEndpoint(smsNumber));
			} catch (Throwable e) {
				e.printStackTrace();
				syncEngine.addMessage("Unexpected Error: " + e.getMessage());
			}
		}
	}

	public static String getDefaultSmsNumber() {
		String smsPort = JavaRosaServiceProvider.instance().getPropertyManager().getSingularProperty(Mesh4jPropertyRules.SMS_PORT);
		String smsTarget = JavaRosaServiceProvider.instance().getPropertyManager().getSingularProperty(Mesh4jPropertyRules.SMS_DEFAULT_TARGET);
		return "sms://"+smsTarget+":"+smsPort;
	}

	public static void importFormDefFromURL() {
		String url = JavaRosaServiceProvider.instance().getPropertyManager().getSingularProperty(Mesh4jPropertyRules.HTTP_DISCOVERY_XFORMS_URL) + "?format=atom10";
		HttpSyncAdapter httpAdapter = new HttpSyncAdapter(url, AtomSyndicationFormat.INSTANCE, LoggedUserIdentityProvider.INSTANCE, IdGenerator.INSTANCE);
		Vector<Item> items = httpAdapter.getAll();
			
		for (Item item : items) {
			if(!item.isDeleted()){
				String xml = item.getContent().getPayload();
				FormDefUtils.importFormDefFromXml(xml);
			}				
		}
	}

	public static void importFormDefFromSMS() throws UnavailableServiceException {
		String smsNumber = SyncEngineService.getDefaultSmsNumber();
		SyncEngineService syncEngine = (SyncEngineService) JavaRosaServiceProvider.instance().getService(SyncEngineService.NAME);
		if(syncEngine != null){
			SplitAdapter splitAdapter = makeXFormDefSplitAdapter();
			MessageSyncAdapter adapter = new MessageSyncAdapter(FORM_DEF_XFORM_NAME, LoggedUserIdentityProvider.INSTANCE, splitAdapter);
			syncEngine.smsSyncEngine.synchronize(adapter, new SmsEndpoint(smsNumber));
		}
	}
	
	public static void cancelSync(String formName, String smsNumber) throws UnavailableServiceException{
		SyncEngineService syncEngine = (SyncEngineService) JavaRosaServiceProvider.instance().getService(SyncEngineService.NAME);
		if(syncEngine != null){
			syncEngine.smsSyncEngine.cancelSync(formName, new SmsEndpoint(smsNumber));
		}
	}

	public static void deleteInfo(boolean deleteXforms) throws UnavailableServiceException{
		for (String name : RecordStore.listRecordStores()) {
			boolean noOk = true;
			int i = 0;
			while(noOk && i < 5){
				try{
					if(name.startsWith("M4X_")){
						RecordStore.deleteRecordStore(name);	
					}
					noOk = false;
				}catch (Exception e) {
					i++;
					e.printStackTrace();
				}
			}
		}
		
		SyncEngineService service = (SyncEngineService)JavaRosaServiceProvider.instance().getService(NAME);
		service.smsSyncEngine.cleanAllSession();

		if(deleteXforms){
			DataModelTreeRMSUtility dataModel = (DataModelTreeRMSUtility)JavaRosaServiceProvider.instance().getStorageManager().getRMSStorageProvider().getUtility(DataModelTreeRMSUtility.getUtilityName());
			dataModel.tempEmpty();
			
			FormDefRMSUtility formDef = (FormDefRMSUtility)JavaRosaServiceProvider.instance().getStorageManager().getRMSStorageProvider().getUtility(FormDefRMSUtility.getUtilityName());
			formDef.tempEmpty();
			
			initializeDefaultDefForms();
		}
	}

	public static void initializeDefaultDefForms() {
		FormDefRMSUtility formDef = (FormDefRMSUtility)JavaRosaServiceProvider.instance().getStorageManager().getRMSStorageProvider().getUtility(FormDefRMSUtility.getUtilityName());
		if (formDef.getNumberOfRecords() == 0) {
			formDef.writeToRMS(XFormUtils.getFormFromResource("/mesh4j_example.xhtml"));
			formDef.writeToRMS(XFormUtils.getFormFromResource("/mesh4j_example_1.xhtml"));
			formDef.writeToRMS(XFormUtils.getFormFromResource("/hmis-a_draft.xhtml"));
//			formDef.writeToRMS(XFormUtils.getFormFromResource("/hmis-b_draft.xhtml"));
			
			boolean useTemplates = JavaRosaServiceProvider.instance().getPropertyManager().getSingularProperty(Mesh4jPropertyRules.USE_TEMPLATES).equals("yes") ? true : false;
			if(useTemplates){
				formDef.writeToRMS(XFormUtils.getFormFromResource("/XFormDemo1.xhtml"));
				formDef.writeToRMS(XFormUtils.getFormFromResource("/XFormDemo2.xhtml"));
			}
		}
	}
	
	public static void forceSyncRetries() throws UnavailableServiceException {
		SyncEngineService syncEngine = (SyncEngineService) JavaRosaServiceProvider.instance().getService(SyncEngineService.NAME);
		ISmsChannel channel = (ISmsChannel)syncEngine.smsSyncEngine.getChannel();
		channel.resendPendingACKOutcommingBatches(0);
		channel.sendAskForRetryIncompleteIncommingBatches(0);
	}
	
	// IMessageSyncAdapterFactory METHODS
	public IMessageSyncAdapter createSyncAdapter(String sourceId) {
		SplitAdapter splitAdapter = makeXFormSplitAdapter(sourceId);
		MessageSyncAdapter adapter = new MessageSyncAdapter(sourceId, LoggedUserIdentityProvider.INSTANCE, splitAdapter);
		return adapter;
	}
	
	// IMessageSyncAware METHODS
	public void beginSync(ISyncSession syncSession) {
		this.addMessage("Begin Sync session id: " + syncSession.getSessionId() + " source: " + syncSession.getSourceId() + " endpoint: " + syncSession.getTarget().getEndpointId());
	}

	public void beginSyncWithError(ISyncSession syncSession) {
		this.addMessage("Protocol Error: Imposible begin Sync because there are a sync session active with this endopoint: " + syncSession.getTarget().getEndpointId()+ " source: " + syncSession.getSourceId());
	}
	
	public void endSync(ISyncSession syncSession, Vector<Item> conflicts) {
		String error = conflicts.isEmpty() ? "" : " with " + conflicts.size() + " conflicts";
		this.addMessage("End Sync session id: " + syncSession.getSessionId() + " source: " + syncSession.getSourceId() + " endpoint: " + syncSession.getTarget().getEndpointId() + " " + error);
	}

	public void notifyInvalidMessageProtocol(IMessage message) {
		this.addMessage("Protocol Error: message was discarded because protocol header is invalid: " + message.getProtocol() + " message type: " + translateMessageType(message) + " session: " + message.getSessionId() + " endpoint: " + message.getEndpoint().getEndpointId());
	}

	public void notifyInvalidProtocolMessageOrder(IMessage message) {
		this.addMessage("Protocol Error: message was discarded because sync session does not exist or sync session is closed: " + translateMessageType(message) + " session: " + message.getSessionId() + " endpoint: " + message.getEndpoint().getEndpointId());
	}

	public void notifyMessageProcessed(IMessage message, Vector<IMessage> response) {
		StringBuffer sb = new StringBuffer();
		if(response.size() > 0){
			sb.append(" response: ");
			IMessage responseMessage;
			
			if(response.size() == 1){
				responseMessage = response.elementAt(0);
				sb.append(translateMessageType(responseMessage));
			} else {
				for (int i = 0; i < response.size(); i++) {
					responseMessage = response.elementAt(i);
					sb.append(i);
					sb.append(" - ");
					sb.append(translateMessageType(responseMessage));
					if(i < response.size() -1){
						sb.append(" | ");
					}
				}
			}
		}
		this.addMessage("Process message: " + translateMessageType(message) + " origin: " +  message.getOrigin() + " session: " + message.getSessionId() + " endpoint: " + message.getEndpoint().getEndpointId() + sb.toString());
	}

	public void notifyProblemWithSessionCreation(IMessage message) {
		SyncEngineService syncEngine;
		try {
			syncEngine = (SyncEngineService) JavaRosaServiceProvider.instance().getService(SyncEngineService.NAME);
			String sourceId = syncEngine.smsSyncEngine.getProtocol().getBeginMessageProcessor().getSourceId(message.getData());
			this.addMessage("Protocol Error: message was discarded because session can not be created: " + translateMessageType(message) + " session: " + message.getSessionId() + " endpoint: " + message.getEndpoint().getEndpointId() + " source: " + sourceId);
		} catch (UnavailableServiceException e) {
			e.printStackTrace();
			throw new MeshException("SyncEngineService was not available");
		}
	}
	
	public void notifyCancelSync(ISyncSession syncSession) {
		this.addMessage("Cancel Sync: " + syncSession.getSessionId()+ " endpoint: " + syncSession.getTarget().getEndpointId() + " source: " + syncSession.getSourceId());
	}

	public void notifyCancelSyncErrorSyncSessionNotOpen(String sourceId, IEndpoint endpoint) {
		this.addMessage("Protocol Error: Cancel sync failed, there are not an open session for endpoint: " + endpoint.getEndpointId() + " source: " + sourceId);
	}
	
	private String translateMessageType(IMessage message){
		String messageType = message.getMessageType();
		if(BeginSyncMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			return "Begin sync";
		}
		if(NoChangesMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			return "No Changes";
		}
		if(LastVersionStatusMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			return "Last Items Versions";
		}
		if(GetForMergeMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			String syncId = GetForMergeMessageProcessor.getSyncID(message.getData());
			return "Get For Merge: " + syncId;
		}
		if(MergeMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			String syncId = ItemEncoding.getSyncID(message.getData());
			return "Merge: " + syncId;
		}
		if(MergeWithACKMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			String itemData = message.getData().substring(1, message.getData().length());
			String syncId = ItemEncoding.getSyncID(itemData);
			return "Merge with ACK: " + syncId;
		}
		if(EndSyncMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			return "End sync";
		}
		if(ACKEndSyncMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			return "ACK of End Sync";
		}
		if(ACKMergeMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			String itemData = message.getData().substring(1, message.getData().length());
			String syncId = ItemEncoding.getSyncID(itemData);
			return "ACK of Merge: " + syncId;
		}
		if(CancelSyncMessageProcessor.MESSAGE_TYPE.equals(messageType)){
			return "Cancel sync";
		}
		return messageType;
	}
	
	// ISmsRetiesNotification methods
	public void notifyReSendBatch(SmsMessageBatch batch) {
		this.addMessage("Re-send batch: " + batch.getId());
	}

	public void notifyReSendIncompleteReceivedBatch(SmsMessageBatch batch) {
		this.addMessage("Re-send incomplete received batch: " + batch.getId());
	}

	public void notifySendAskForRetryIncompleteReceivedBatch(SmsMessageBatch batch) {
		this.addMessage("Ask for incomplete received batch: " + batch.getId());
	}
	
	public void notifyReSendBatchError(SmsMessageBatch batch, String error) {
		this.addMessage("Protocol Error while re-send batch: " + batch.getId() + " error: " + error);	
	}

	public void notifySendAskForRetryError(SmsMessageBatch batch, String error) {
		this.addMessage("Protocol Error while ask for incomplete received batch: " + batch.getId() + " error: " + error);
	}

	public void notifyNoIncompleteIncommingBatches() {
		this.addMessage("Ask for incomplete received batch no generate result because there are not incomplete incomming batches");
	}

	public void notifyNoPendingACKOutcommingBatches() {
		this.addMessage("Re-send batches no generate result because there are not outcomming batches pending for ACK");
		
	}

	// ISmsConnectionInboundOutboundNotification METHODS
	public void notifyReceiveMessage(String endpointId, String message,
			Date date) {
		this.addMessage("Received sms from: " + endpointId + " message: " + message);
	}


	public void notifyReceiveMessageError(String endpointId, String message,
			Date date, String error) {
		this.addMessage("Sms Error: Can not process received sms from: " + endpointId + " message: " + message + " error: " + error);
	}


	public void notifySendMessage(String endpointId, String message) {
		this.addMessage("Send sms to: " + endpointId + " message: " + message);
	}


	public void notifySendMessageError(String endpointId, String message, String error) {
		this.addMessage("Sms Error: Can not send sms to: " + endpointId + " message: " + message + " error: " + error);
	}

	
	// CONSOLE METHODS
	private void addMessage(String message){
		synchronized (SEMAPHORE) {
			this.consoleRepository.add(message);	
		}		
	}
	
	public static void cleanConsole() {
		SyncEngineService syncEngine;
		try {
			syncEngine = (SyncEngineService) JavaRosaServiceProvider.instance().getService(SyncEngineService.NAME);
			synchronized (SEMAPHORE) {
				syncEngine.consoleRepository.deleteAll();	
			}
		} catch (UnavailableServiceException e) {
			e.printStackTrace();
			throw new MeshException("SyncEngineService was not available");
		}
	}
	
	public static Vector<String> getConsoleItems() {
		try {
			SyncEngineService syncEngine = (SyncEngineService) JavaRosaServiceProvider.instance().getService(SyncEngineService.NAME);
			synchronized (SEMAPHORE) {
				return syncEngine.consoleRepository.getAll();
			}
		} catch (UnavailableServiceException e) {
			e.printStackTrace();
			throw new MeshException("SyncEngineService was not available");
		}
	}

}
